//! The WASI embedding API definitions for Wasmtime.

use crate::wasm_byte_vec_t;
use anyhow::Result;
use bytes::Bytes;
use std::ffi::{CStr, c_char, c_void};
use std::fs::File;
use std::path::Path;
use std::pin::Pin;
use std::slice;
use std::task::{Context, Poll};
use tokio::io::{self, AsyncWrite};
use wasmtime_wasi::WasiCtxBuilder;
use wasmtime_wasi::p1::WasiP1Ctx;
use wasmtime_wasi_io::streams::StreamError;

unsafe fn cstr_to_path<'a>(path: *const c_char) -> Option<&'a Path> {
    CStr::from_ptr(path).to_str().map(Path::new).ok()
}

unsafe fn cstr_to_str<'a>(s: *const c_char) -> Option<&'a str> {
    CStr::from_ptr(s).to_str().ok()
}

unsafe fn open_file(path: *const c_char) -> Option<File> {
    File::open(cstr_to_path(path)?).ok()
}

unsafe fn create_file(path: *const c_char) -> Option<File> {
    File::create(cstr_to_path(path)?).ok()
}

#[repr(C)]
pub struct wasi_config_t {
    builder: WasiCtxBuilder,
}

wasmtime_c_api_macros::declare_own!(wasi_config_t);

impl wasi_config_t {
    pub fn into_wasi_ctx(mut self) -> Result<WasiP1Ctx> {
        Ok(self.builder.build_p1())
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_new() -> Box<wasi_config_t> {
    Box::new(wasi_config_t {
        builder: WasiCtxBuilder::new(),
    })
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_set_argv(
    config: &mut wasi_config_t,
    argc: usize,
    argv: *const *const c_char,
) -> bool {
    for arg in slice::from_raw_parts(argv, argc) {
        let arg = match CStr::from_ptr(*arg).to_str() {
            Ok(s) => s,
            Err(_) => return false,
        };
        config.builder.arg(arg);
    }
    true
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_inherit_argv(config: &mut wasi_config_t) {
    config.builder.inherit_args();
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_set_env(
    config: &mut wasi_config_t,
    envc: usize,
    names: *const *const c_char,
    values: *const *const c_char,
) -> bool {
    let names = slice::from_raw_parts(names, envc);
    let values = slice::from_raw_parts(values, envc);

    for (k, v) in names.iter().zip(values) {
        let k = match cstr_to_str(*k) {
            Some(s) => s,
            None => return false,
        };
        let v = match cstr_to_str(*v) {
            Some(s) => s,
            None => return false,
        };
        config.builder.env(k, v);
    }
    true
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_inherit_env(config: &mut wasi_config_t) {
    config.builder.inherit_env();
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_set_stdin_file(
    config: &mut wasi_config_t,
    path: *const c_char,
) -> bool {
    let file = match open_file(path) {
        Some(f) => f,
        None => return false,
    };

    let file = tokio::fs::File::from_std(file);
    let stdin_stream = wasmtime_wasi::cli::AsyncStdinStream::new(file);
    config.builder.stdin(stdin_stream);

    true
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_set_stdin_bytes(
    config: &mut wasi_config_t,
    binary: &mut wasm_byte_vec_t,
) {
    let binary = binary.take();
    let binary = wasmtime_wasi::p2::pipe::MemoryInputPipe::new(binary);
    config.builder.stdin(binary);
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_inherit_stdin(config: &mut wasi_config_t) {
    config.builder.inherit_stdin();
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_set_stdout_file(
    config: &mut wasi_config_t,
    path: *const c_char,
) -> bool {
    let file = match create_file(path) {
        Some(f) => f,
        None => return false,
    };

    config
        .builder
        .stdout(wasmtime_wasi::cli::OutputFile::new(file));

    true
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_inherit_stdout(config: &mut wasi_config_t) {
    config.builder.inherit_stdout();
}

struct CustomOutputStreamInner {
    foreign_data: crate::ForeignData,
    callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
}

impl CustomOutputStreamInner {
    pub fn raw_write(&self, buf: &[u8]) -> io::Result<usize> {
        let wrote = (self.callback)(self.foreign_data.data, buf.as_ptr(), buf.len());

        if wrote >= 0 {
            Ok(wrote as _)
        } else {
            Err(io::Error::from_raw_os_error(wrote.abs() as _))
        }
    }
}

#[derive(Clone)]
pub struct CustomOutputStream {
    inner: std::sync::Arc<CustomOutputStreamInner>,
}

impl CustomOutputStream {
    pub fn new(
        foreign_data: crate::ForeignData,
        callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
    ) -> Self {
        Self {
            inner: std::sync::Arc::new(CustomOutputStreamInner {
                foreign_data,
                callback,
            }),
        }
    }
}

#[async_trait::async_trait]
impl wasmtime_wasi::p2::Pollable for CustomOutputStream {
    async fn ready(&mut self) {}
}

#[async_trait::async_trait]
impl wasmtime_wasi::p2::OutputStream for CustomOutputStream {
    fn write(&mut self, bytes: Bytes) -> Result<(), StreamError> {
        let wrote = self
            .inner
            .raw_write(&bytes)
            .map_err(|e| StreamError::LastOperationFailed(e.into()))?;

        if wrote != bytes.len() {
            return Err(StreamError::LastOperationFailed(anyhow::anyhow!(
                "Partial writes in wasip2 implementation are not allowed"
            )));
        }

        Ok(())
    }
    fn flush(&mut self) -> Result<(), StreamError> {
        Ok(())
    }
    fn check_write(&mut self) -> Result<usize, StreamError> {
        Ok(usize::MAX)
    }
}

impl AsyncWrite for CustomOutputStream {
    fn poll_write(
        self: Pin<&mut Self>,
        _cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        Poll::Ready(self.inner.raw_write(buf))
    }
    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Poll::Ready(Ok(()))
    }
    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Poll::Ready(Ok(()))
    }
}

impl wasmtime_wasi::cli::IsTerminal for CustomOutputStream {
    fn is_terminal(&self) -> bool {
        false
    }
}

impl wasmtime_wasi::cli::StdoutStream for CustomOutputStream {
    fn async_stream(&self) -> Box<dyn AsyncWrite + Send + Sync> {
        Box::new(self.clone())
    }
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_set_stdout_custom(
    config: &mut wasi_config_t,
    callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
    data: *mut c_void,
    finalizer: Option<extern "C" fn(*mut c_void)>,
) {
    config.builder.stdout(CustomOutputStream::new(
        crate::ForeignData { data, finalizer },
        callback,
    ));
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_set_stderr_file(
    config: &mut wasi_config_t,
    path: *const c_char,
) -> bool {
    let file = match create_file(path) {
        Some(f) => f,
        None => return false,
    };

    config
        .builder
        .stderr(wasmtime_wasi::cli::OutputFile::new(file));

    true
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_inherit_stderr(config: &mut wasi_config_t) {
    config.builder.inherit_stderr();
}

#[unsafe(no_mangle)]
pub extern "C" fn wasi_config_set_stderr_custom(
    config: &mut wasi_config_t,
    callback: extern "C" fn(*mut c_void, *const u8, usize) -> isize,
    data: *mut c_void,
    finalizer: Option<extern "C" fn(*mut c_void)>,
) {
    config.builder.stderr(CustomOutputStream::new(
        crate::ForeignData { data, finalizer },
        callback,
    ));
}

#[unsafe(no_mangle)]
pub unsafe extern "C" fn wasi_config_preopen_dir(
    config: &mut wasi_config_t,
    path: *const c_char,
    guest_path: *const c_char,
    dir_perms: usize,
    file_perms: usize,
) -> bool {
    let guest_path = match cstr_to_str(guest_path) {
        Some(p) => p,
        None => return false,
    };

    let host_path = match cstr_to_path(path) {
        Some(p) => p,
        None => return false,
    };

    let dir_perms = match wasmtime_wasi::DirPerms::from_bits(dir_perms) {
        Some(p) => p,
        None => return false,
    };

    let file_perms = match wasmtime_wasi::FilePerms::from_bits(file_perms) {
        Some(p) => p,
        None => return false,
    };

    config
        .builder
        .preopened_dir(host_path, guest_path, dir_perms, file_perms)
        .is_ok()
}
